package jwanalytics.ejb.webhouse;

import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.ejb.EJB;
import javax.ejb.Stateful;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import jwanalytics.ejb.dataobjects.JWAPageHitEvent;
import jwanalytics.utils.date.BaseCalendar;

/**
 * This is the entry point bean for all calls to log web sessions web analytics.
 * 
 * This bean is bound one to one with web sessions.  Every web session has one stub for its own instance of this EJB.
 * 
 * Each web client calls this bean synchronously to log web events.  
 * This bean calls other beans synchronously or asynchronously to persist the web analytics.
 *   
 * The technique of having this bean calling others asynchronously will improve responsiveness of your web pages.
 * 
 * @author Andre Cesta aacesta@yahoo.com
 */
@Stateful
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class WebSessionAnalyticsLogger implements WebSessionAnalyticsLoggerLocal {
	
	@PersistenceContext EntityManager entityManager;
	
	@EJB private DateAndTimeFacadeLocal dateAndTimeFacade;
	
	private List<PageFact> openPagesStack = new ArrayList<PageFact>(2); 
	
	/** When this one is null we are on the first request/first page hit. */
	private ClickStreamPageEventFact clickStreamPageEventFact = null;
	
	/** Locally cached here for reuse in subsequent logPageHit calls. */
	private ReturningBrowserDimension returningBrowserDimension = null;
	
	/** Locally cached here for reuse in subsequent logPageHit calls. */	
	private SessionDimension sessionDimension = null;	
	
	/** Locally cached here for reuse in subsequent logPageHit calls. */
	private ClientSystemDimension clientSystemDimension = null;	
	
	/** Locally cached here for reuse in subsequent logPageHit calls. */
	private RefererDimension refererDimension = null;
	
	/** Locally cached here for reuse in subsequent logPageHit calls. THis is also always the last page visited. */
	private PageDimension pageDimension = null;	
	
	/** THis will store the date time of the first pageHit. */
	private Date firstPageHitTime = null; 
	
	/** Flag for first request. Only for one page hit is true, afterwards it is always false. Explicitely managing the first
	 * request flag makes code clearer and less reliable on null values of variables for initialization. */
	private boolean firstRequest = true;
	
	/** This will log a complete JWAEvent, usually an instance of JWAPageHitEvent. */
	public void logPageHit(JWAPageHitEvent pageHit, boolean synchronous) {
		if (firstRequest) {
			firstPageHitTime = pageHit.getEventReceivedDateTime();
		}
		
		Integer dateKey = dateAndTimeFacade.getDateDimensionKey(pageHit.getEventReceivedDateTime());

		Integer timeOfDayKey = BaseCalendar.timeIndex(pageHit.getEventReceivedDateTime());
		
		clickStreamPageEventFact = doClickStreamPageEventFactStart(pageHit, dateKey, timeOfDayKey);
		
		returningBrowserDimension = doReturningBrowserDimension(pageHit, dateKey, timeOfDayKey, clickStreamPageEventFact);
		
		sessionDimension = doSessionDimension(pageHit, dateKey, timeOfDayKey, clickStreamPageEventFact);
		
		if (sessionDimension.getId() == null || sessionDimension.getId() <= 0) {
			throw new IllegalStateException("Cannot use session id which is:"+sessionDimension.getId());
		}
		
		pageDimension = doPageDimension(pageHit, dateKey, timeOfDayKey, clickStreamPageEventFact);
		
		//This part above needs to be done synchronously as everything needs a session, page, browser.
		
		//Below can be recorded assynchronously, so we could send each persistance task to a message driven bean and just identify
		//what needs to be persisted or not here.
		
		doPageFact(pageHit, dateKey, timeOfDayKey, pageDimension.getId(), sessionDimension.getId(), clickStreamPageEventFact);
		
		clientSystemDimension = doClientSystemDimension(pageHit, dateKey, timeOfDayKey, clickStreamPageEventFact);
		
		refererDimension = doRefererDimension(pageHit, dateKey, timeOfDayKey, clickStreamPageEventFact);
		
		doClickStreamPageEventFactEnd(pageHit, pageDimension, dateKey, timeOfDayKey, clickStreamPageEventFact); //always leave database in a consistent state with an up-to-date clickstream fact (in case it crashes, etc).
		
		firstRequest = false;
	}
	
	
	/** Hook method subclasses can extend. */
	private ReturningBrowserDimension doReturningBrowserDimension(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		//Now lazy-initialize returning_browser_dimension?
		if (firstRequest) {
			ReturningBrowserDimension tmpReturningBrowserDimension = null;
			//Check if this returning browser already exists on the database,
			if (pageHit.getReturningBrowserKey() != null) {
				tmpReturningBrowserDimension = entityManager.find(ReturningBrowserDimension.class, pageHit.getReturningBrowserKey());
			}
			
			// if not existent insert it.
			if (tmpReturningBrowserDimension == null) {
				clickStreamPageEventFactParam.setIsReturningBrowser('n');
				tmpReturningBrowserDimension = new ReturningBrowserDimension();				
				tmpReturningBrowserDimension.setCookieDateKey(dateKey);
				entityManager.persist(tmpReturningBrowserDimension);
				entityManager.flush();
			} else {
				clickStreamPageEventFactParam.setIsReturningBrowser('y');				
			}
			
			clickStreamPageEventFactParam.setReturningBrowserKey(tmpReturningBrowserDimension.getId()); //new info, have to save it.
			return tmpReturningBrowserDimension;
		} else {
			return returningBrowserDimension;
		}
	}
	
	
	/** . */
	public SessionDimension doSessionDimension(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		//Now lazy-initialize session_dimesion?
		if (firstRequest) {
			SessionDimension tmpSessionDimension = null;
			if (pageHit.getSessionKey() != null) {
				tmpSessionDimension = entityManager.find(SessionDimension.class, pageHit.getSessionKey());
			}
			
			if (tmpSessionDimension == null) {
				tmpSessionDimension = new SessionDimension();
				entityManager.persist(tmpSessionDimension);
				entityManager.flush();
			}
			
			clickStreamPageEventFactParam.setSessionKey(tmpSessionDimension.getId()); //new information, have to set.
			return tmpSessionDimension;
		} else {
			return sessionDimension;
		}
	}
	
	
	public PageDimension doPageDimension(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		//Do page_dimension
		Query query = entityManager.createQuery("from PageDimension p where p.page = :page");
		query.setParameter("page", pageHit.getPageName() != null? pageHit.getPageName() : "");
		PageDimension tmpPageDimension = null;
		try {
			tmpPageDimension = (PageDimension)query.getSingleResult(); 
		} catch(javax.persistence.NoResultException exception) {
			//ignore.
		}
		
		if (tmpPageDimension == null) {
			tmpPageDimension = new PageDimension();
			tmpPageDimension.setPage(pageHit.getPageName() != null?pageHit.getPageName():"");
			entityManager.persist(tmpPageDimension);
			entityManager.flush();
		}
		
		if (firstRequest) {
			clickStreamPageEventFactParam.setStartPageKey(tmpPageDimension.getId());
		}
		
		return tmpPageDimension;  //this is also the last page visited.
	}
	
	
	/** */
	public PageFact doPageFact(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey, Long pageKey, Long sessionKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		PageFact tmpPageFact = new PageFact();
		tmpPageFact.setPageKey(pageKey);
		tmpPageFact.setPageSeconds(0); //so far 0 seconds spent on this page, persist it like this and later update it with more seconds.
		tmpPageFact.setServerTimeMilliseconds(pageHit.getEventReceivedDateTime()); //store the time this request was received (earliest we record).
		tmpPageFact.setSessionKey(sessionKey);
		tmpPageFact.setTimeOfDayKey(timeOfDayKey);
		tmpPageFact.setWebServerDateKey(dateKey);
		entityManager.persist(tmpPageFact); //here we could send asynchronously to a message queue to be persisted.
		entityManager.flush();
		
		//close any pages considered opened (assume this new page hit means other pages were closed).  There should be only one though (business rule).
		while (openPagesStack.size() > 0) {
			doDequeuePageFact(openPagesStack, pageHit.getEventReceivedDateTime().getTime(), clickStreamPageEventFactParam);
		}
		
		openPagesStack.add(tmpPageFact); //add the page to queue until a new page is clicked and this one is considered done with/read by the user.
		return tmpPageFact;
	}	
	
	
	/** . */
	public ClientSystemDimension doClientSystemDimension(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		//Now initialize client_system_dimension
		if (firstRequest) { //this does not exist yet in the database.
			ClientSystemDimension tmpClientSystemDimension = null;			
			Query query = entityManager.createQuery("from ClientSystemDimension c where c.userAgent = :userAgent and c.acceptLanguage = :acceptLanguage");
			query.setParameter("userAgent", pageHit.getRequest().getHeader("User-Agent"));
			query.setParameter("acceptLanguage", pageHit.getRequest().getHeader("Accept-Language"));
			try {
				tmpClientSystemDimension = (ClientSystemDimension)query.getSingleResult(); 
			} catch(javax.persistence.NoResultException exception) {
				//ignore.
			}
			
			if (tmpClientSystemDimension == null) {
				tmpClientSystemDimension = new ClientSystemDimension();
				tmpClientSystemDimension.setUserAgent(pageHit.getRequest().getHeader("User-Agent"));
				tmpClientSystemDimension.setAcceptLanguage(pageHit.getRequest().getHeader("Accept-Language"));
				entityManager.persist(tmpClientSystemDimension); //here we could send asynchronously to a message queue to be persisted.
				entityManager.flush();
			}
			
			clickStreamPageEventFactParam.setClientSystemKey(tmpClientSystemDimension.getId());
			
			return tmpClientSystemDimension;
		} else {
			return clientSystemDimension;
		}
	}
	
	
	public RefererDimension doRefererDimension(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		if (firstRequest) {
			RefererDimension tmpRefererDimension = null;
			//Now initialize referer_dimension
			String origReferer = pageHit.getRequest().getHeader("Referer");
			String url = "";
			String domain = "";
			String tmpDomain = "";
			if (origReferer == null) {
				origReferer = ""; //This will symbolize the no referer situation (empty referer string on refere_dimension table also).
			} else { //origReferer != null
				String referer = origReferer;
				//Maybe a relative URL.
				if (referer.toLowerCase().trim().indexOf("http://") != 0) {
					referer = "http://dummydomain/"+referer;
				}
				//Use URL class for parsing logic.
				URL refURL;
				try {
					refURL = new URL(referer);
					tmpDomain = refURL.getHost();
					if (tmpDomain == null || tmpDomain.equals("dummydomain")) tmpDomain = ""; //this symbolizes no domain.
					//Will get the referer if it already exists in the database, will create it new if it does not exist.
				} catch(Exception e) {
					//
				}
			}
			
			domain = tmpDomain;
			url = origReferer;
			Query query = entityManager.createQuery("from RefererDimension r where r.url = :url and r.domain = :domain");
			query.setParameter("url", url);
			query.setParameter("domain", domain);
			try {
				tmpRefererDimension = (RefererDimension)query.getSingleResult(); 
			} catch(javax.persistence.NoResultException exception) {
				//ignore.
			}
			
			if (tmpRefererDimension == null) { //this does not exist yet in the database.
				tmpRefererDimension = new RefererDimension();
				tmpRefererDimension.setUrl(url);
				tmpRefererDimension.setDomain(domain);
				entityManager.persist(tmpRefererDimension); //here we could send asynchronously to a message queue to be persisted.
				entityManager.flush();
			}
			
			clickStreamPageEventFactParam.setRefererKey(tmpRefererDimension.getId());
			
			return tmpRefererDimension;
		} else {
			return refererDimension;
		}
		
	}
	
	
	/** Hook method, subclasses can extend, to start the click stream page event fact, but not persist it. 
	 * @see doClickStreamPageEventFactEnd. */
	private ClickStreamPageEventFact doClickStreamPageEventFactStart(JWAPageHitEvent pageHit, int dateKey, int timeOfDayKey) {
		if (firstRequest) {
			ClickStreamPageEventFact tmpClickStreamPageEventFact = new ClickStreamPageEventFact(); 
			tmpClickStreamPageEventFact = new ClickStreamPageEventFact();
			tmpClickStreamPageEventFact.setAveragePageTime(0);
			tmpClickStreamPageEventFact.setPagesVisited(0);			
			tmpClickStreamPageEventFact.setStartDateKey(dateKey);
			tmpClickStreamPageEventFact.setStartTimeOfDayKey(timeOfDayKey);
			tmpClickStreamPageEventFact.setIpAddress("'"+pageHit.getRequest().getRemoteAddr()+"'");
			return tmpClickStreamPageEventFact;
		} else {
			return clickStreamPageEventFact;
		}
	}
	
	
	/** @TODO: work also with page heart-beat (Javascript that keeps pooling server) instead of assuming that when a new page is opened the old one is closed. */
	public Long doDequeuePageFact(List<PageFact> queue, Long eventReceivedDateTime, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		PageFact supposedlyClosedPageFact = queue.remove(0);
		int supposedlyClosedPageSeconds = 0;
		
		if (supposedlyClosedPageFact != null) {
			supposedlyClosedPageFact.setPageSeconds((int)((eventReceivedDateTime - supposedlyClosedPageFact.getServerTimeMilliseconds().getTime()) / 1000));
			supposedlyClosedPageSeconds = supposedlyClosedPageFact.getPageSeconds();
			entityManager.merge(supposedlyClosedPageFact); //here we could send asynchronously to a message queue to be persisted.
			entityManager.flush(); //we do real time web analytics.
			clickStreamPageEventFactParam.setTotalPageTime(clickStreamPageEventFactParam.getTotalPageTime() + supposedlyClosedPageSeconds);			
		}		
		
		return supposedlyClosedPageFact.getId();
	}
	
	
	/**
	 * doClickStreamPageEventFactEnd(pageHit, dateKey, timeOfDayKey);
	 */
	public void doClickStreamPageEventFactEnd(JWAPageHitEvent pageHit, PageDimension pageHitPageParam, int dateKey, int timeOfDayKey, ClickStreamPageEventFact clickStreamPageEventFactParam) {
		try {
			clickStreamPageEventFactParam.setSessionSeconds((int)((new Date().getTime() - firstPageHitTime.getTime()) / 1000));
			clickStreamPageEventFactParam.setPagesVisited(clickStreamPageEventFactParam.getPagesVisited() + 1); //assume this last page hit also (parameter lastPage).
			clickStreamPageEventFactParam.setEndPageKey(pageHitPageParam.getId());
			clickStreamPageEventFactParam.setAveragePageTime(clickStreamPageEventFactParam.getTotalPageTime() / clickStreamPageEventFactParam.getPagesVisited());
			if (firstRequest) {
				entityManager.persist(clickStreamPageEventFactParam);
			} else {
				entityManager.merge(clickStreamPageEventFact);
			}
			entityManager.flush(); //we do real time web analytics, this session needs to be available to update models for new users.
		} catch(javax.persistence.PersistenceException e) {
			e.printStackTrace();
		}
	}
	
	
	public Long getReturningBrowserId() {
		if (returningBrowserDimension == null) {
			return null;
		} else {
			return returningBrowserDimension.getId();
		}
	}
	
	
	/** This is the web analytics session id. */
	public Long getSessionId() {
		if (sessionDimension == null) {
			return null;
		} else {
			return sessionDimension.getId();
		}
	}
	
}
